if ( !FBDEditor ) FBDEditor = {};

FBDEditor.model = function () {
/*
    model for FBDEditor 1.0
    Copyright (C) 2008-2009 Yuriy Butenko y.v.butenko@gmail.com

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
	this.blksDef = {};
}
FBDEditor.model.prototype = {
	"_extend":function ( dst, properties ) {
		// IE does not copy .toString
		for ( var i in properties ) dst[i] = properties[i];
		if ( properties.toString ) dst.toString = properties.toString;
	},
	"clearAll":function ( cntrl ) {
		while ( this._bd ) {
			cntrl.delEntity( this._bd.vHandle );
			this.del( this._bd );
		};
	},
	"save":function () {
		var blks = this.blksDef;
		var s = '{"blocks":{';
		for ( var i in blks ) {
			var inp = '"' + blks[i].inputs.join('","') + '"';
			var outp = '"' + blks[i].outputs.join('","') + '"';
			s += '\n"' + i + '":{"inputs":[' + inp + '],"outputs":[' + outp + ']},';
		};
		s = s.replace(/,$/,'') + '},\n"bd":[';
		for ( var i = this._bd; i; i = i.next ) s += "\n" + i.toString() + ",";
		return s.replace(/,$/,'') +'\n]\n}\n';
	},
	"load":function ( text, cntrl ) {
		this.clearAll( cntrl );
		if ( text == null ) return null;
		try {
			text = eval( "("+text+")" );
			if ( !( "blocks" in text ) || !( "bd" in text ) ) return null;
			for ( var i in text.blocks ) {
				var j = text.blocks[i];
				for ( var k = j.inputs.length; k--; )
					if ( typeof j.inputs[k] != "string" ) return null;
				for ( var k = j.outputs.length; k--; )
					if ( typeof j.outputs[k] != "string" ) return null;
			};
			//cntrl.newLoadedBlocks( text.blocks );
			this.setBlocksDef( text.blocks, cntrl );
			for ( var i = text.bd.length, j, v, m; i--; ) {
				switch ( ( j = text.bd[i] ).tp ) {
				// TODO test input data
					case "trace":
						m = new this.trace(this,j.x0,j.y0,j.x1,j.y1,("l0" in j ? j.l0 : -1),null);
						break;
					case "fb":
						m = new this.fb(this,j.x,j.y,j.text,j.classId,null);
						break;
					case "input":
						m = new this.input( this,j.x,j.y,j.text,null);
						break;
					case "output":
						m = new this.output( this,j.x,j.y,j.text,null);
						break;
					case "consta":
						m = new this.consta( this,j.x,j.y,j.text,null);
						break;
					case "vari":
						m = new this.vari( this,j.x,j.y,j.text,null);
						break;
				};
				m.vHandle = cntrl.createEntity( j, m );
				if ( "label" in j ) m.vHandle.id = j.label;		// for DEMO only (dirty CODE)
			};
		} catch ( e ) {
			return null;
		};
		return true;
	},
	"getCode":function () {
		var res = { "v":[], "f":[], "i":[], "o":[], "txt":[] };
		var vars = [], varIds = {}, traces = {};
		function getTx ( x, y ) {	// return [ entity, id ]
			for ( var p = null, f, i = traces; i; p = i,i = i.next ) {
				if (( f = x == i.x0 && y == i.y0 ) || x == i.x1 && y == i.y1 ) {
					x = f ? i.x1 : i.x0;
					y = f ? i.y1 : i.y0;
					if ( p ) p.next = i.next;	// del trace
					else traces = i.next;
					for (var id, i = 0; i < vars.length; i++ ) {
						if ( typeof( id = vars[i].getOutput(x,y) ) == "string" )return [vars[i],id]
					};
					throw new Error();
				};
			};
			return null;
		};
		// get entities and generate init
		for ( var cn, i = this._bd; i; i = i.next ) {
			if ( i instanceof this.trace ) {
				traces={ "x0":i.x, "y0":i.y, "x1":i.x + i.dX, "y1":i.y + i.dY, "next":traces };
			} else {
				vars.push( i );
				if ( i instanceof this.consta || i.txt in varIds ) continue;
				varIds[ i.txt ] = null;
				res[i instanceof this.fb ? "f" : 
					i instanceof this.vari ? "v" :
					i instanceof this.input ? "i" :
					"o"
				].push( i instanceof this.fb ? [ i.txt, i.classId ] : i.txt );
			};
		};
		// sort not traces by coordinates
		vars.sort( function ( a, b ) {
				return a.y != b.y ? a.y - b.y : a.x - b.x;
		} );
		// generate code
		for ( var i = 0; i < vars.length; i++ ) {
			var e = vars[ i ];
			if ( e instanceof this.consta ) continue;	// const have not inputs
			for ( var tx, otp, k = 0; otp = e.getInput( k ); k++ ) {	// for all inputs
				try {
					tx = getTx( otp[0], otp[1] );
				} catch ( err ) {
					throw new Error( 'Input "' + e.txt + '.' + ( otp[2] || "" ) + '" is not connect' );
				};
				if ( !tx ) continue;	// input not conected
				res.txt.push( [tx[0] instanceof this.consta?"cset":"set",
					e.txt/*id*/,otp[2] || null/*field*/,tx[0].txt/*id*/,tx[1]/*field*/] );
			};
			if ( e instanceof this.fb ) res.txt.push( ["call",e.txt,e.classId] );
		};
		return res;
	},
	"createTrace":function ( x0, y0, x1, y1, l0, vHandle ) {
		return new this.trace( this, x0, y0, x1, y1, l0, vHandle );
	},
	"trace":function ( ctxt, x0, y0, x1, y1, l0, vHandle ) {
		ctxt._proto.call( this, ctxt, x0, y0, x1 - x0, y1 - y0, vHandle );
		this.l0 = l0;
	},
	"createFB":function ( x, y, id, classId, vHandle ) {
		return new this.fb( this, x, y, id, classId, vHandle );
	},
	"fb":function ( ctxt, x, y, id, classId, vHandle ) {
		ctxt._proto.call( this, ctxt, x, y, 112, 16, vHandle );
		this.txt = id;
		this.classId = classId;
		// TODO get dX & dY from view
		this.dX = 112;
		this.dY = Math.max( ctxt.blksDef[classId].inputs.length, ctxt.blksDef[classId].outputs.length, 1 ) * 16;
	},
	"createInput":function ( x, y, id, vHandle ) {
		return new this.input( this, x, y, id, vHandle );
	},
	"createOutput":function ( x, y, id, vHandle ) {
		return new this.output( this, x, y, id, vHandle );
	},
	"createVari":function ( x, y, id, vHandle ) {
		return new this.vari( this, x, y, id, vHandle );
	},
	"createConsta":function ( x, y, val, vHandle ) {
		return new this.consta( this, x, y, val, vHandle );
	},
	"vari":function ( ctxt, x, y, id, vHandle ) {
		ctxt._proto.call( this, ctxt, x, y, 112, 16, vHandle );
		this.txt = id;
	},
	"consta":function ( ctxt, x, y, val, vHandle ) {
		ctxt._proto.call( this, ctxt, x, y, 112, 16, vHandle );
		this.txt = val;
	},
	"input":function ( ctxt, x, y, id, vHandle ) {
		ctxt._proto.call( this, ctxt, x, y, 112, 16, vHandle );
		this.txt = id;
	},
	"output":function ( ctxt, x, y, id, vHandle ) {
		ctxt._proto.call( this, ctxt, x, y, 112, 16, vHandle );
		this.txt = id;
	},
	"del":function ( h ) {
		h.del();
	},
	"moveBy":function ( h, dX, dY ) {
		h.moveBy( dX, dY );
		return h;
	},
	"copy":function ( h ) {
		return h.copy();
	},
	"_bd":null,
	"_proto":function ( prnt, x, y, dX, dY, vHandle ) {
		if ( !arguments.length ) return;
		this.x = x;
		this.y = y;
		this.dX = dX;
		this.dY = dY;
		prnt._bd && ( prnt._bd.prev = this );
		this.next = ( this.prnt = prnt )._bd;
		prnt._bd = this;
		this.vHandle = vHandle;
	},
	"getType":function ( h ) {
		return h instanceof this.trace ? "trace" :
			h instanceof this.fb ? "fb" :
			h instanceof this.vari ? "vari" :
			h instanceof this.consta ? "consta" :
			h instanceof this.input ? "input" :
			"output";
	},
	"blksDef":null,
	"getVHandle":function ( h ) {
		return h.vHandle;
	},
	"setVHandle":function ( h, vH ) {
		h.vHandle = vH;
	},
	"getX":function ( h ) {
		return h.x;
	},
	"getY":function ( h ) {
		return h.y;
	},
	"getX1":function ( h ) {
		return h.x + h.dX;
	},
	"getY1":function ( h ) {
		return h.y + h.dY;
	},
	"getL0":function ( h ) {
		return h.l0;
	},
	"regenTrace":function ( h, x0, y0, x1, y1, l0 ) {
		h.x = x0;
		h.y = y0;
		h.dX = x1 - x0;
		h.dY = y1 - y0;
		h.l0 = l0;
		return h;
	},
	"getId":function ( h ) {
		return h.txt || null;
	},
	"setId":function ( h, txt ) {
		h.txt = txt;
		return h;
	},
	"getClassId":function ( h ) {
		return h.classId || null;
	},
	"getBlockDef":function ( id ) {
		return this.blksDef[id];
	},
	"setBlocksDef":function ( def, cntrl ) {	// format: {elements}  element:{'inputs':[..],'outputs':[..]};
		var isEqArrays = function ( a, b ) {
			if ( a.length != b.length ) return false;
			for ( var i = 0, l = a.length; i < l; i++ ) if ( a[i] != b[i] ) return false;
			return true;
		};
		for ( var i in this.blksDef ) {
			if ( i in def ) {
				if ( !isEqArrays(def[i].inputs,this.blksDef[i].inputs)
					|| !isEqArrays(def[i].outputs,this.blksDef[i].outputs) ) {
					this.blksDef[i] = def[i];
					for ( var j = this._bd; j; j = j.next )
						if ( j.classId && j.classId == i ) {
							// TODO get dY from view
							j.dY = Math.max( def[i].inputs.length, def[i].outputs.length, 1 ) * 16;
							j.vHandle = cntrl.redrawFB( j.x, j.y, j.txt, i, def[i].inputs, def[i].outputs, j.vHandle );	// -->
	
						};
				};
			} else {
				cntrl.delOldBlock( i );		// -->
				delete this.blksDef[i];
				for ( var next = this._bd, j; j = next; ) {
					next = next.next;
					if ( j instanceof this.fb && j.classId == i ) {
						cntrl.delEntity( this.getVHandle( j ) );	// -->
						this.del( j );
					};
				};
			};
		};
		for ( var i in def ) {
			if ( !( i in this.blksDef ) ) {
				cntrl.addNewBlock( i );		// -->
				this.blksDef[i] = def[i];
			};
		};
	},
	"getBlocksDef":function () {
		var res = {}, b = this.blksDef;
		for ( var i in b ) res[i] = {'inputs':b[i].inputs.slice(0),'outputs':b[i].outputs.slice(0)};
		return res;
	},
	"select":function ( x0, y0, x1, y1 ) {
		var a = Math.min( x0, x1 ), b = Math.max( x0, x1 );
		x0 = a;
		x1 = b;
		var a = Math.min( y0, y1 ), b = Math.max( y0, y1 );
		y0 = a;
		y1 = b;
		var sel = [], i = this._bd;
//		alert('x0:'+x0+' y0:'+y0+' x1:'+x1+' y1:'+y1);
		while ( i ) {
//			alert('ix0:'+i.x+' iy0:'+i.y+' ix1:'+(i.x+i.dX)+' iy1:'+(i.y+i.dY));
			if ( i.x >= x0 && i.x <= x1	&& i.y >= y0 && i.y <= y1
				&& i.x + i.dX >= x0 && i.x + i.dX <= x1
				&& i.y + i.dY >= y0 && i.y + i.dY <= y1 )
				sel.push( i.vHandle );
			i = i.next;
		};
		return sel;
	}
}
FBDEditor.model.prototype._proto.prototype = {
	"x":0,
	"y":0,
	"dX":16,
	"dY":16,
	"prnt":null,
	"next":null,
	"prev":null,
	"vHandle":null,
	"del":function () {
		this.next && ( this.next.prev = this.prev );
		if ( this.prev ) this.prev.next = this.next;
		else this.prnt._bd = this.next;
		this.next = this.prev = this.prnt = null;
		this.vHandle = null;
	},
	"copy":function () {
		throw( new Error( "Model: not defined 'clone' method" ) );
	},
	"setXY":function ( x, y ) {
		this.x = x;
		this.y = y;
	},
	"moveBy":function ( dX, dY ) {
		this.x += dX;
		this.y += dY;
	},
	"setDim":function ( dX, dY ) {
		this.dX = dX;
		this.dY = dY;
	},
	"toString":function () {
		throw( new Error( "Model: not defined 'toString' method" ) );
	},
	"parse":function ( prm ) {
		throw( new Error( "Model: not defined 'parse' method" ) );
	}
}
FBDEditor.model.prototype.trace.prototype = new FBDEditor.model.prototype._proto();
FBDEditor.model.prototype._extend(
	FBDEditor.model.prototype.trace.prototype,
	{
	"toString":function () {
		return '{"tp":"trace", "x0":' + this.x + ', "y0":' + this.y
			+ ', "x1":' + ( this.x + this.dX ) + ', "y1":' + ( this.y + this.dY )
			+ ', "l0":' + this.l0 + '}';
	},
	"parse":function ( prm ) {
		if ( prm.s.charAt( prm.i ) != "t" ) return null;
		var z = / (-?\d{1,5}) (-?\d{1,5}) (-?\d{1,5}) (-?\d{1,5}) (-?\d{1,5})\n/g;
		z.lastIndex = prm.i + 1;
		var res = z.exec( prm.s );
		if ( !res || prm.i + res[0].length != z.lastIndex ) return null;
		prm.i = z.lastIndex;
		return new FBDEditor.model.prototype.trace( parseInt( res[1] ), parseInt( res[2] ), parseInt( res[3] ), parseInt( res[4] ) );
	},
	"copy":function () {
		return new this.prnt.trace( this.prnt, this.x, this.y, this.x + this.dX, this.y + this.dY, this.l0, null );
	}
	}
);
FBDEditor.model.prototype.fb.prototype = new FBDEditor.model.prototype._proto();
FBDEditor.model.prototype._extend(
	FBDEditor.model.prototype.fb.prototype,
	{
	"txt":"",
	"classId":"",
	"toString":function () {
		return '{"tp":"fb", "x":' + this.x + ', "y":' + this.y + ', "text":"' + this.txt
			+ '", "classId":"' + this.classId + '"}';
	},
	"parse":function ( prm, blksDef ) {
		if ( prm.s.charAt( prm.i ) != "f" ) return null;
		var z = / (-?\d{1,5}) (-?\d{1,5}) ([^ ]*) (.*)\n/g;
		z.lastIndex = prm.i + 1;
		var res = z.exec( prm.s );
		if ( !res || prm.i + res[0].length != z.lastIndex || !blksDef[res[3]] ) return null;
		prm.i = z.lastIndex;
		return new FBDEditor.model.prototype.fb( parseInt( res[1] ), parseInt( res[2] ), res[3], res[4] );
	},
	"getInput":function ( n ) {
		var def = this.prnt.blksDef[this.classId];
		return def && n < def.inputs.length ? [this.x, this.y + 8 + 16 * n, def.inputs[n] ] : null;
	},
	"getOutput":function ( x, y ) {
		var def = this.prnt.blksDef[this.classId];
		if ( !def || this.x + this.dX != x || y < this.y + 8 ) return null;
		var n = ( y - this.y - 8 ) / 16
		return n < def.outputs.length && Math.floor( n ) == n ? def.outputs[n] : null;
	},
	"copy":function () {
		return new this.prnt.fb( this.prnt, this.x, this.y, this.txt, this.classId, null );
	}
	}
);
FBDEditor.model.prototype.input.prototype = new FBDEditor.model.prototype._proto();
FBDEditor.model.prototype._extend(
	FBDEditor.model.prototype.input.prototype,
	{
	"txt":"",
	"toString":function () {
		return '{"tp":"input", "x":' + this.x + ', "y":' + this.y + ', "text":"' + this.txt + '"}';
	},
	"parse":function ( prm ) {
		return FBDEditor.model.prototype.vari.prototype._parse("i",prm,"input");
	},
	"copy":function () {
		return new this.prnt.input( this.prnt, this.x, this.y, this.txt, null );
	}
	}
);
FBDEditor.model.prototype.output.prototype = new FBDEditor.model.prototype._proto();
FBDEditor.model.prototype._extend(
	FBDEditor.model.prototype.output.prototype,
	{
	"txt":"",
	"toString":function () {
		return '{"tp":"output", "x":' + this.x + ', "y":' + this.y + ', "text":"' + this.txt + '"}';
	},
	"parse":function ( prm ) {
		return FBDEditor.model.prototype.vari.prototype._parse("o",prm,"output");
	},
	"copy":function () {
		return new this.prnt.output( this.prnt, this.x, this.y, this.txt, null );
	}
	}
);
FBDEditor.model.prototype.vari.prototype = new FBDEditor.model.prototype._proto();
FBDEditor.model.prototype._extend(
	FBDEditor.model.prototype.vari.prototype,
	{
	"txt":"",
	"toString":function () {
		return '{"tp":"vari", "x":' + this.x + ', "y":' + this.y + ', "text":"' + this.txt + '"}';
	},
	"parse":function ( prm ) {
		return FBDEditor.model.prototype.vari.prototype._parse("v",prm,"vari");
	},
	"_parse":function ( c, prm, cons ) {
		if ( prm.s.charAt( prm.i ) != "c" ) return null;
		var z = / (-?\d{1,5}) (-?\d{1,5}) (.*)\n/g;
		z.lastIndex = prm.i + 1;
		var res = z.exec( prm.s );
		if ( !res || prm.i + res[0].length != z.lastIndex ) return null;
		prm.i = z.lastIndex;
		return new FBDEditor.model.prototype[cons]( parseInt( res[1] ), parseInt( res[2] ), res[3] );
	},
	"copy":function () {
		return new this.prnt.vari( this.prnt, this.x, this.y, this.txt, null );
	}
	}
);
FBDEditor.model.prototype.consta.prototype = new FBDEditor.model.prototype._proto();
FBDEditor.model.prototype._extend(
	FBDEditor.model.prototype.consta.prototype,
	{
	"txt":"",
	"toString":function () {
		return '{"tp":"consta", "x":' + this.x + ', "y":' + this.y + ', "text":"' + this.txt.replace(/"/g,'\\"') + '"}';
	},
	"parse":function ( prm ) {
		return FBDEditor.model.prototype.vari.prototype._parse("c",prm,"consta");
	},
	"getInput":function ( n ) {
		return null;
	},
	"copy":function () {
		return new this.prnt.consta( this.prnt, this.x, this.y, this.txt, null );
	}
	}
);
FBDEditor.model.prototype.vari.prototype.getOutput =
FBDEditor.model.prototype.consta.prototype.getOutput =
FBDEditor.model.prototype.input.prototype.getOutput =
FBDEditor.model.prototype.output.prototype.getOutput =
	function ( x, y ) {
		return this.x + this.dX == x && this.y + 8 == y ? "" : null;
	}
FBDEditor.model.prototype.vari.prototype.getInput =
FBDEditor.model.prototype.input.prototype.getInput =
FBDEditor.model.prototype.output.prototype.getInput =
	function ( n ) {
		return n < 1 ? [this.x,this.y + 8] : null;
	}
